// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gsutil

import (
	"bufio"
	"bytes"
	"context"
	"os"
	"path/filepath"
	"strings"

	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/system/environ"
)

const defaultBotoEnvVar = "BOTO_CONFIG"
const defaultBotoPathEnvVar = "BOTO_PATH"
const botoFileBotoBlockHeader = "[Boto]"
const botoFileGSUtilBlockHeader = "[GSUtil]"
const softwareUpdateCheckPeriodKey = "software_update_check_period"
const stateDirKey = "state_dir"

// Boto represents a subset of .boto gsutil configuration file.
type Boto struct {
	botoSection       []byte // [Boto] block of a .boto file.
	gsutilSection     []byte // [GSUtil] block of a .boto file.
	StateDir          string // value of GSUtil.state_dir
	RefreshToken      string // value of Credentials.gs_oauth2_refresh_token
	GCEServiceAccount string // value of GoogleCompute.service_account
	ProviderLabel     string // value of OAuth2.provider_label
	ProviderAuthURI   string // value of OAuth2.provider_authorization_uri
	ProviderTokenURI  string // value of OAuth2.provider_token_uri
}

// Write creates the config file.
func (b *Boto) Write(path string) error {
	buf := bytes.Buffer{}

	line := func(s string) {
		buf.WriteString(s)
		buf.WriteRune('\n')
	}

	opts := func(name, value string) {
		if value != "" {
			buf.WriteString(name)
			buf.WriteString(" = ")
			buf.WriteString(value)
			buf.WriteRune('\n')
		}
	}

	line("# Autogenerated by LUCI. Do not edit.")
	line("")
	line("[GSUtil]")
	opts(softwareUpdateCheckPeriodKey, "0")
	opts(stateDirKey, b.StateDir)
	if b.gsutilSection != nil {
		s := bufio.NewScanner(bytes.NewBuffer(b.gsutilSection))
		for s.Scan() {
			ln := s.Text()
			// skip lines required by this tool.
			if strings.HasPrefix(ln, softwareUpdateCheckPeriodKey) ||
				strings.HasPrefix(ln, stateDirKey) {
				continue
			}
			line(ln)
		}
	}
	if b.RefreshToken != "" {
		line("")
		line("[Credentials]")
		opts("gs_oauth2_refresh_token", b.RefreshToken)
	}
	if b.GCEServiceAccount != "" {
		line("")
		line("[GoogleCompute]")
		opts("service_account", b.GCEServiceAccount)
	}
	if b.ProviderLabel != "" || b.ProviderAuthURI != "" || b.ProviderTokenURI != "" {
		line("")
		line("[OAuth2]")
		opts("provider_label", b.ProviderLabel)
		opts("provider_authorization_uri", b.ProviderAuthURI)
		opts("provider_token_uri", b.ProviderTokenURI)
	}

	if b.botoSection != nil {
		line("")
		line(botoFileBotoBlockHeader)
		buf.Write(b.botoSection)
	}

	return os.WriteFile(path, buf.Bytes(), 0600)
}

func (b *Boto) readBotoConfigSections(path string) error {
	f, err := os.Open(path)
	if err != nil {
		return errors.Fmt("failed to read existing .boto file at %s: %w", path, err)
	}

	defer func() { _ = f.Close() }()

	sections := make(map[string][]byte)

	buf := bytes.Buffer{}

	s := bufio.NewScanner(f)
	sectionFound := ""
	for s.Scan() {
		ln := s.Text()

		if strings.HasPrefix(strings.TrimLeft(ln, " \t"), "#") {
			continue
		}

		if sectionFound != "" && strings.HasPrefix(ln, "[") {
			sections[sectionFound] = buf.Bytes()
			buf = bytes.Buffer{}
			sectionFound = ""
			continue
		}

		if ln == botoFileBotoBlockHeader {
			sectionFound = botoFileBotoBlockHeader
			continue
		}

		if ln == botoFileGSUtilBlockHeader {
			sectionFound = botoFileGSUtilBlockHeader
			continue
		}

		if sectionFound != "" {
			buf.WriteString(ln)
			buf.WriteRune('\n')
			continue
		}
	}

	if sectionFound != "" {
		sections[sectionFound] = buf.Bytes()
	}

	if err := s.Err(); err != nil {
		return errors.Fmt("failed to read existing .boto file at %s: %w", path, err)
	}

	for k, s := range sections {
		if k == botoFileBotoBlockHeader {
			b.botoSection = s
			continue
		}

		if k == botoFileGSUtilBlockHeader {
			b.gsutilSection = s
			continue
		}
	}

	return nil
}

// mock home dir for tests so we can still use environ.Env.
var homeDirKey = "mocked os.UserHomeDir"

func withMockHomeDir(ctx context.Context, dir string) context.Context {
	return context.WithValue(ctx, &homeDirKey, dir)
}

func userHomeDir(ctx context.Context) (string, error) {
	val, _ := ctx.Value(&homeDirKey).(string)
	if val != "" {
		return val, nil
	}
	return os.UserHomeDir()
}

// findUserBotoConfig finds a users boto config if configured.
// https://cloud.google.com/storage/docs/boto-gsutil#location
func findUserBotoConfig(ctx context.Context) (string, error) {
	env := environ.FromCtx(ctx)
	exists := func(path string) bool {
		_, err := os.Stat(path)
		return err == nil
	}

	// 1. BOTO_CONFIG env var
	if p := env.Get(defaultBotoEnvVar); p != "" {
		if exists(p) {
			return p, nil
		}
	}

	// 2. BOTO_PATH
	if p := env.Get(defaultBotoPathEnvVar); p != "" {
		paths := strings.Split(p, string(os.PathListSeparator))
		for i := len(paths) - 1; i >= 0; i-- {
			if exists(paths[i]) {
				return paths[i], nil
			}
		}
	}

	// 3. $HOME/.boto
	homeDir, err := userHomeDir(ctx)
	if err != nil {
		return "", err
	}

	if h := filepath.Join(homeDir, ".boto"); exists(h) {
		return h, nil
	}

	return "", nil
}

// PrepareStateDir prepares a directory (based on b.StateDir) for gsutil to keep
// its state and drops .boto config there. If `botoPath` is set the [Boto] block
// of the configuration is included in the new .boto config.

// PrepareStateDir returns a path to the created .boto file.
func PrepareStateDir(ctx context.Context, b *Boto) (string, error) {
	if err := os.MkdirAll(b.StateDir, 0700); err != nil {
		return "", errors.Fmt("failed to create gsutil state dir at %s: %w", b.StateDir, err)
	}

	// Find the path to the users boto config if it exists.
	botoPath, err := findUserBotoConfig(ctx)
	if err != nil {
		return "", err
	}

	if botoPath != "" {
		// Copy the boto config blocks into the newly generated config. These blocks
		// contains configuration unrelated to authentication.
		err := b.readBotoConfigSections(botoPath)
		if err != nil && !os.IsNotExist(err) {
			return "", err
		}
	}

	botoCfg := filepath.Join(b.StateDir, ".boto")
	if err := b.Write(botoCfg); err != nil {
		return "", errors.Fmt("failed to write %s: %w", botoCfg, err)
	}

	// Make sure the credentials cache file is empty, otherwise it will grow
	// after each server launch, since it uses refresh_token (which we may
	// generate randomly) as a cache key. We don't really need this cache anyway.
	if err := os.Remove(filepath.Join(b.StateDir, "credstore")); err != nil && !os.IsNotExist(err) {
		return "", errors.Fmt("failed to remove credstore: %w", err)
	}

	return botoCfg, nil
}
